﻿// The MIT License (MIT)
// Copyright (c) 2010 Nick Darnell
//
// http://www.nickdarnell.com
// http://wpfthemereplacer.codeplex.com

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;

namespace QuixoticLabs.Presentation
{
    /// <summary>
    /// The <see cref="ThemeReplacer"/> allows you to replace any standard WPF Theme dictionary with
    /// your own theme dictionary.  If you replace an existing theme, make sure you provide styles for
    /// all of the controls that were previously covered.  Not providing a style may cause the control
    /// to be completely unstyled.
    /// </summary>
    public static class ThemeReplacer
    {
        /// <summary>
        /// The WM_THEMECHANGED message is broadcast to every window following a theme change event. Examples 
        /// of theme change events are the activation of a theme, the deactivation of a theme, or a transition 
        /// from one theme to another.
        /// </summary>
        private const int WM_THEMECHANGED = 0x31a;

        private static Assembly PresentationAssembly = typeof(FrameworkElement).Assembly;
        private static Assembly WindowsBaseAssembly = typeof(Dispatcher).Assembly;

        private static bool m_initialized;
        private static Dictionary<Assembly, ResourceDictionary> m_themes;
        private static DispatcherTimer m_themecheck;

        static ThemeReplacer()
        {
            m_themes = new Dictionary<Assembly, ResourceDictionary>();
            m_themecheck = new DispatcherTimer(DispatcherPriority.Background);

            m_themecheck.Interval = TimeSpan.FromSeconds(1);
            m_themecheck.Tick += new EventHandler(OnThemeCheckTick);
        }

        /// <summary>
        /// Gets a reference to the PresentationFramework assembly.  This is just here
        /// for simplicity/utility.
        /// </summary>
        public static Assembly PresentationFramework
        {
            get { return PresentationAssembly; }
        }

        /// <summary>
        /// Initializes the theme replacer, this will replace any themes with already
        /// added themes, and any new themes added will be automatically start being
        /// used.
        /// </summary>
        public static void Initialize()
        {
            if (m_initialized)
                return;

            m_initialized = true;

            LoadThemes();

            m_themecheck.Start();
        }

        /// <summary>
        /// Shuts down the theme replacer but does not clear any of the custom
        /// themes.  This will restore your application to the default theme.
        /// </summary>
        public static void Shutdown()
        {
            if (!m_initialized)
                return;

            m_initialized = false;

            m_themecheck.Stop();

            RefreshTheme();
        }

        /// <summary>
        /// Gets a value indiciating if the theme replacer has been initialized.  No
        /// custom theme will be used until Initialize() has been called.
        /// </summary>
        public static bool IsInitialized
        {
            get { return m_initialized; }
        }

        /// <summary>
        /// Adds a new theme dictionary for all of the controls in an assembly.
        /// </summary>
        /// <remarks>Themes for controls are based on the assembly they originate 
        /// in.  So if you wanted to replace the theme for the standard controls, 
        /// you would pass in a reference to the PresentationFramework assembly and
        /// the corresponding theme dictionary to use.</remarks>
        /// <param name="assembly">The assembly to replace the theme for.</param>
        /// <param name="resources">The resource dictionary to use.</param>
        /// <example>ThemeReplacer.AddTheme(ThemeReplacer.PresentationFramework, myTheme);</example>
        public static void AddTheme(Assembly assembly, ResourceDictionary resources)
        {
            m_themes.Remove(assembly);
            m_themes.Add(assembly, resources);

            if (IsInitialized)
            {
                LoadTheme(assembly, resources);
                RefreshTheme();
            }
        }

        /// <summary>
        /// Removes the custom theme dictionary override for an assembly.
        /// </summary>
        /// <param name="assembly">The assembly to remove the theme for.</param>
        public static void RemoveTheme(Assembly assembly)
        {
            m_themes.Remove(assembly);

            if (IsInitialized)
            {
                RefreshTheme();
            }
        }

        /// <summary>
        /// Removes all custom theme dictionaries.
        /// </summary>
        public static void ClearThemes()
        {
            m_themes.Clear();

            if (IsInitialized)
            {
                RefreshTheme();
            }
        }

        /// <summary>
        /// Changes the current theme that WPF thinks it should be attempting to
        /// match by loading theme dictionaries matching themeName(.themeColor).
        /// An example would be aero.normalcolor; themeName=aero, themeColor=normalcolor
        /// However, you do not need to specify a real theme, if you're attempting to 
        /// load your own theme dictionaries in other assemblies that you own, while 
        /// simultaneously using AddTheme to replace the theme dictionaries for
        /// the assemblies you don't control.
        /// </summary>
        /// <param name="themeName">The theme name to use.</param>
        public static void ChangeTheme(string themeName)
        {
            ChangeTheme(themeName, string.Empty);
        }

        /// <summary>
        /// Changes the current theme that WPF thinks it should be attempting to
        /// match by loading theme dictionaries matching themeName(.themeColor).
        /// An example would be aero.normalcolor; themeName=aero, themeColor=normalcolor
        /// However, you do not need to specify a real theme, if you're attempting to 
        /// load your own theme dictionaries in other assemblies that you own, while 
        /// simultaneously using AddTheme to replace the theme dictionaries for
        /// the assemblies you don't control.
        /// </summary>
        /// <param name="themeName">The theme name to use.</param>
        /// <param name="themeColor">The theme color to use</param>
        public static void ChangeTheme(string themeName, string themeColor)
        {
            SystemColorsEx.InvalidateCache();
            SystemParametersEx.InvalidateCache();

            SystemResources.OnThemeChanged();

            if (!string.IsNullOrEmpty(themeName))
            {
                UxThemeWrapper._isActive = true;
                UxThemeWrapper._themeName = themeName;
                UxThemeWrapper._themeColor = themeColor;
            }

            SystemResources.InvalidateResources(false);

            LoadThemes();
        }

        /// <summary>
        /// Loads all currently loaded custom themes and signals WPF to reload
        /// the current theme.
        /// </summary>
        private static void LoadThemes()
        {
            foreach (KeyValuePair<Assembly, ResourceDictionary> entry in m_themes)
            {
                if (!IsCustomThemeUsed(entry.Key, entry.Value))
                {
                    LoadTheme(entry.Key, entry.Value);
                }
            }

            RefreshTheme();
        }

        /// <summary>
        /// Loads the theme, replacing the existing one in the theme dictionary.
        /// </summary>
        /// <param name="assembly">The assembly to replace the theme for.</param>
        /// <param name="resources">The theme dictionary to use.</param>
        private static void LoadTheme(Assembly assembly, ResourceDictionary resources)
        {
            ResourceDictionaries themeEntry = GetThemeEntry(assembly);

            themeEntry._themedDictionary = resources;
            themeEntry._themedLoaded = true;

            Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                (Action)delegate()
                {
                    if (!IsCustomThemeUsed(assembly, resources))
                    {
                        LoadTheme(assembly, resources);
                    }
                });
        }

        /// <summary>
        /// Determines if the user theme is being used.
        /// </summary>
        /// <param name="assembly">The assembly to check.</param>
        /// <param name="resources">The resource dictionary to compare against the current one.</param>
        /// <returns><c>true</c> if the custom theme is being used, otherwise <c>false</c>.</returns>
        private static bool IsCustomThemeUsed(Assembly assembly, ResourceDictionary resources)
        {
            ResourceDictionaries themeEntry = GetThemeEntry(assembly);
            return themeEntry._themedDictionary == resources;
        }

        /// <summary>
        /// Gets the entry in the theme dictionary for the given assembly.
        /// </summary>
        /// <param name="assembly">The assembly to use.</param>
        /// <returns>The entry in the theme dictionary for the assembly.</returns>
        private static ResourceDictionaries GetThemeEntry(Assembly assembly)
        {
            // We do this so that WPF will go ahead and load the theme normally
            // in case it has not yet been loaded.  This will fill in the other
            // information we care about like the generic dictionary...etc
            SystemResources.EnsureDictionarySlot(assembly);

            // We need to get the currently loaded theme dictionary entry
            return SystemResources.GetResourceDictionaries(assembly);
        }

        [DllImport("user32.dll")]
        private extern static int SendMessage(IntPtr hwnd, uint msg, uint wParam, uint lParam);

        [DllImport("user32.dll")]
        private static extern int EnumWindows(CallBackPtr callPtr, int lPar);

        private delegate bool CallBackPtr(IntPtr hwnd, int lParam);

        /// <summary>
        /// Notifies all top level windows that the window theme has changed.  This
        /// causes WPF to re-request the information from the theme dictionary.
        /// </summary>
        private static void RefreshTheme()
        {
            EnumWindows(
                (CallBackPtr)delegate(IntPtr hwnd, int lParam)
                    {
                        SendMessage(hwnd, WM_THEMECHANGED, 0, 0);
                        return true;
                    },
                0);
        }

        private static void OnThemeCheckTick(object sender, EventArgs e)
        {
            bool foundMissingTheme = false;
            foreach (KeyValuePair<Assembly, ResourceDictionary> entry in m_themes)
            {
                if (!IsCustomThemeUsed(entry.Key, entry.Value))
                {
                    LoadTheme(entry.Key, entry.Value);
                    foundMissingTheme = true;
                }
            }

            if (foundMissingTheme)
                RefreshTheme();
        }

        private static class SystemParametersEx
        {
            private readonly static MethodInfo __InvalidateCache = typeof(SystemParameters).GetMethod("InvalidateCache", BindingFlags.NonPublic | BindingFlags.Static, null, Type.EmptyTypes, null);

            public static void InvalidateCache()
            {
                __InvalidateCache.Invoke(null, null);
            }
        }

        private static class SystemColorsEx
        {
            private readonly static MethodInfo __InvalidateCache = typeof(SystemColors).GetMethod("InvalidateCache", BindingFlags.NonPublic | BindingFlags.Static);

            public static void InvalidateCache()
            {
                __InvalidateCache.Invoke(null, null);
            }
        }

        private static class SystemResources
        {
            private static Type SystemResourcesType = PresentationAssembly.GetType("System.Windows.SystemResources");

            private static FieldInfo __dictionaries = SystemResourcesType.GetField("_dictionaries", BindingFlags.NonPublic | BindingFlags.Static);
            private static FieldInfo __hwndNotify = SystemResourcesType.GetField("_hwndNotify", BindingFlags.NonPublic | BindingFlags.Static);

            private static MethodInfo __EnsureDictionarySlot = SystemResourcesType.GetMethod("EnsureDictionarySlot", BindingFlags.NonPublic | BindingFlags.Static);

            private static MethodInfo __EnsureResourceChangeListener = SystemResourcesType.GetMethod("EnsureResourceChangeListener", BindingFlags.NonPublic | BindingFlags.Static);
            private static MethodInfo __InvalidateResources = SystemResourcesType.GetMethod("InvalidateResources", BindingFlags.NonPublic | BindingFlags.Static);
            private static MethodInfo __OnThemeChanged = SystemResourcesType.GetMethod("OnThemeChanged", BindingFlags.NonPublic | BindingFlags.Static);

            public static object _dictionaries
            {
                get { return __dictionaries.GetValue(null); }
                set { __dictionaries.SetValue(null, value); }
            }

            public static object _hwndNotify
            {
                get { return __hwndNotify.GetValue(null); }
                set { __hwndNotify.SetValue(null, value); }
            }

            public static void EnsureDictionarySlot(Assembly assembly)
            {
                __EnsureDictionarySlot.Invoke(null, new object[] { assembly });
            }

            public static void EnsureResourceChangeListener()
            {
                __EnsureResourceChangeListener.Invoke(null, null);
            }

            public static void InvalidateResources(bool isSysColorsOrSettingsChange)
            {
                __InvalidateResources.Invoke(null, new object[] { isSysColorsOrSettingsChange });
            }

            public static void OnThemeChanged()
            {
                __OnThemeChanged.Invoke(null, null);
            }

            public static ResourceDictionaries GetResourceDictionaries(Assembly assembly)
            {
                // Grab a reference to the _dictionaries field.  This contains a dictionary
                // of assembly-to-resourcedictionaries mapping.
                object _dictionaries = __dictionaries.GetValue(null);

                PropertyInfo item = _dictionaries.GetType().GetProperty("Item", BindingFlags.Instance | BindingFlags.Public);
                return new ResourceDictionaries(item.GetValue(_dictionaries, new object[]{ assembly }));
            }
        }

        private class ResourceDictionaries
        {
            private static FieldInfo __themedDictionary;
            private static FieldInfo __themedLoaded;

            private object m_dictionaries;

            public ResourceDictionaries(object dictionaries)
            {
                m_dictionaries = dictionaries;

                if (__themedDictionary == null)
                    __themedDictionary = m_dictionaries.GetType().GetField("_themedDictionary", BindingFlags.NonPublic | BindingFlags.Instance);

                if (__themedLoaded == null)
                    __themedLoaded = m_dictionaries.GetType().GetField("_themedLoaded", BindingFlags.NonPublic | BindingFlags.Instance);
            }

            public ResourceDictionary _themedDictionary
            {
                get { return (ResourceDictionary)__themedDictionary.GetValue(m_dictionaries); }
                set { __themedDictionary.SetValue(m_dictionaries, value); }
            }

            public bool _themedLoaded
            {
                get { return (bool)__themedLoaded.GetValue(m_dictionaries); }
                set { __themedLoaded.SetValue(m_dictionaries, value); }
            }
        }

        private static class UxThemeWrapper
        {
            private static Type UxThemeWrapperType = PresentationAssembly.GetType("MS.Win32.UxThemeWrapper", true);
            private static Type HwndWrapper = WindowsBaseAssembly.GetType("MS.Win32.HwndWrapper", true);
            private static Type SecurityCriticalDataClass_HwndWrapper = WindowsBaseAssembly.GetType("MS.Internal.SecurityCriticalDataClass`1[MS.Win32.HwndWrapper]", true);

            private static FieldInfo __isActive = UxThemeWrapperType.GetField("_isActive", BindingFlags.NonPublic | BindingFlags.Static);
            private static FieldInfo __themeColor = UxThemeWrapperType.GetField("_themeColor", BindingFlags.NonPublic | BindingFlags.Static);
            private static FieldInfo __themeName = UxThemeWrapperType.GetField("_themeName", BindingFlags.NonPublic | BindingFlags.Static);

            private static PropertyInfo __value = SecurityCriticalDataClass_HwndWrapper.GetProperty("Value", BindingFlags.NonPublic | BindingFlags.Instance);
            private static MethodInfo __addHook = HwndWrapper.GetMethod("AddHook", BindingFlags.Public | BindingFlags.Instance);

            static UxThemeWrapper()
            {
                SystemResources.EnsureResourceChangeListener();

                Delegate hook = Delegate.CreateDelegate(WindowsBaseAssembly.GetType("MS.Win32.HwndWrapperHook"),
                    typeof(UxThemeWrapper).GetMethod("FilterThemeMessage", BindingFlags.NonPublic | BindingFlags.Static));

                object notify = SystemResources._hwndNotify;
                notify = __value.GetValue(notify, null);
                __addHook.Invoke(notify, new object[] { hook });
            }

            public static bool _isActive
            {
                get { return (bool)__isActive.GetValue(null); }
                set { __isActive.SetValue(null, value); }
            }

            public static string _themeColor
            {
                get { return (string)__themeColor.GetValue(null); }
                set { __themeColor.SetValue(null, value); }
            }

            public static string _themeName
            {
                get { return (string)__themeName.GetValue(null); }
                set { __themeName.SetValue(null, value); }
            }

            private static IntPtr FilterThemeMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
            {
                if (msg == WM_THEMECHANGED)
                {
                    handled = true;
                }

                return IntPtr.Zero;
            }
        }
    }
}